﻿using MvvmCross.Core.ViewModels;
using Sidvall.Business;
using Sidvall.Data;
using Sidvall.UI.Entities;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows.Input;

namespace Sidvall.UI.ViewModels
{
    public abstract class EntityViewModelBase<TEntity> : ViewModelBase, INotifyDataErrorInfo
        where TEntity : BusinessBase
    {
        #region Public Members

        public event System.EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        #region ActiveEntity

        private TEntity _ActiveEntity;
        public TEntity ActiveEntity
        {
            get
            {
                return _ActiveEntity;
            }
            protected set
            {
                OnActiveEntityChanging(_ActiveEntity, value);
                _ActiveEntity = value;
                OnActiveEntityChanged(BusinessObjectChangeMode.NewBusinessObject, null, null);
                RaisePropertyChanged(nameof(ActiveEntity));
                if (this.ActiveEntity != null)
                    this.IsSavable = GetIsSavableResolved(this.ActiveEntity);
            }
        }

        #endregion
        #region DataRowBusinessRules

        private BusinessRuleCollection _DataRowBusinessRules;
        public BusinessRuleCollection DataRowBusinessRules
        {
            get
            {
                if (_DataRowBusinessRules == null)
                {
                    lock (_Mutex)
                    {
                        if (_DataRowBusinessRules == null)
                        {
                            var items = new BusinessRuleCollection();
                            items.BusinessRulePropertyChanged += OnDataRowBusinessRulesPropertyChanged;
                            _DataRowBusinessRules = items;
                        }
                    }
                }
                return _DataRowBusinessRules;
            }
        }

        #endregion
        #region IsSavable

        private bool _IsSavable;
        public bool IsSavable
        {
            get
            {
                return _IsSavable;
            }
            set
            {
                if (_IsSavable == value)
                    return;
                _IsSavable = value;
                OnIsSavableChanged();
                RaisePropertyChanged(nameof(IsSavable));
            }
        }

        #endregion
        // Protected
        protected abstract string ViewModelResource { get; }
        #region ViewModelSettings

        private System.Lazy<EntityViewModelSettings> _ViewModelSettings = new System.Lazy<EntityViewModelSettings>(EntityViewModelSettings.CreateDefaultSettings);
        protected EntityViewModelSettings ViewModelSettings
        {
            get
            {
                return _ViewModelSettings.Value;
            }
        }

        #endregion

        // Load item
        #region LoadCommand

        public ICommand LoadCommand
        {
            get
            {
                return new MvxCommand(async () => await DoLoadAsync());
            }
        }

        #endregion
        #region DoLoadAsync

        public async Task DoLoadAsync()
        {
            Task task;

            var exceptions = new System.Collections.Generic.List<System.Exception>();
            using (var manager = new Sidvall.Data.BusyManager(this, "Loading...", "EntityViewModelBase.DoLoadAsync"))
            {
                var dataPortalContext = Sidvall.Business.DataPortalManager.NewContext(this.ViewModelResource, Sidvall.Security.Operations.GetItems, null, default(TEntity));
                try
                {
                    task = OnBeforeLoadItemAsync(dataPortalContext);
                    if (task != null)
                    {
                        await task;
                    }
                    else
                    {
                        OnBeforeLoadItem(dataPortalContext);
                        await dataPortalContext.DoTaskAsync();
                    }
                }
                catch (System.Exception ex)
                {
                    exceptions.Add(ex);
                }
                try
                {
                    if (dataPortalContext.Result == Sidvall.Business.DataPortalResult.None)
                    {
                        dataPortalContext.ResultValueItem = await LoadItemAsync();
                        dataPortalContext.Result = DataPortalResult.Ok;
                    }
                }
                catch (System.Exception ex)
                {
                    exceptions.Add(ex);
                    dataPortalContext.Result = DataPortalResult.Failed;
                }
                try
                {
                    task = OnAfterLoadItemAsync(dataPortalContext);
                    if (task != null)
                    {
                        await task;
                    }
                    else
                    {
                        OnAfterLoadItem(dataPortalContext);
                        await dataPortalContext.DoTaskAsync();
                    }
                    if (dataPortalContext.Result == Sidvall.Business.DataPortalResult.Ok)
                        this.ActiveEntity = dataPortalContext.ResultValueItem;
                }
                catch (System.Exception ex)
                {
                    exceptions.Add(ex);
                }
                try
                {
                    task = OnLoadItemCompletedAsync(dataPortalContext);
                    if (task != null)
                    {
                        await task;
                    }
                    else
                    {
                        OnLoadItemCompleted(dataPortalContext);
                        await dataPortalContext.DoTaskAsync();
                    }
                }
                catch (System.Exception ex)
                {
                    exceptions.Add(ex);
                }
            }
            await LogAsync(exceptions);
            await Sidvall.UI.SystemContext.Current.DialogManager.ShowDialogErrorMessageAsync(Sidvall.Resources.ResourceKeyManager.Current.LoadErrorTitle, exceptions);
        }

        #endregion
        #region OnBeforeLoadItem

        protected virtual void OnBeforeLoadItem(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
        }

        #endregion
        #region OnBeforeLoadItemAsync

        protected virtual Task OnBeforeLoadItemAsync(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
            return null;
        }

        #endregion
        #region LoadItemAsync

        protected virtual Task<TEntity> LoadItemAsync()
        {
            return null;
        }

        #endregion
        #region OnAfterLoadItem

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "AfterLoad")]
        protected virtual void OnAfterLoadItem(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
        }

        #endregion
        #region OnAfterLoadItemAsync

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "AfterLoad")]
        protected virtual Task OnAfterLoadItemAsync(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
            return null;
        }

        #endregion
        #region OnLoadItemCompleted

        protected virtual void OnLoadItemCompleted(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
        }

        #endregion
        #region OnLoadItemCompletedAsync

        protected virtual Task OnLoadItemCompletedAsync(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
            return null;
        }

        #endregion

        // Save item
        #region SaveCommand

        public ICommand SaveCommand
        {
            get
            {
                return new MvxCommand(async () => await DoSaveAsync());
            }
        }

        #endregion
        #region DoSaveAsync

        public async Task DoSaveAsync()
        {
            Task task;
            System.Collections.Generic.List<System.Exception> exceptions;
            global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext;

            using (var manager = new Sidvall.Data.BusyManager(this, "Saving...", "EntityViewModelBase.DoSaveAsync"))
            {
                exceptions = new System.Collections.Generic.List<System.Exception>();
                dataPortalContext = Sidvall.Business.DataPortalManager.NewSaveItemContext(this.ViewModelResource, null, this.ActiveEntity, Data.SaveMode.Default);
                try
                {
                    task = OnBeforeSaveItemAsync(dataPortalContext);
                    if (task != null)
                    {
                        await task;
                    }
                    else
                    {
                        OnBeforeSaveItem(dataPortalContext);
                        await dataPortalContext.DoTaskAsync();
                    }
                }
                catch (System.Exception ex)
                {
                    exceptions.Add(ex);
                }
                try
                {
                    if (dataPortalContext.Result == Sidvall.Business.DataPortalResult.None)
                    {
                        dataPortalContext.ResultValueItem = await SaveItemAsync(dataPortalContext.OperationContext);
                        dataPortalContext.Result = DataPortalResult.Ok;
                    }
                }
                catch (System.Exception ex)
                {
                    exceptions.Add(ex);
                    dataPortalContext.Result = DataPortalResult.Failed;
                }
                try
                {
                    task = OnAfterSaveItemAsync(dataPortalContext);
                    if (task != null)
                    {
                        await task;
                    }
                    else
                    {
                        OnAfterSaveItem(dataPortalContext);
                        await dataPortalContext.DoTaskAsync();
                    }
                    if (dataPortalContext.Result == Sidvall.Business.DataPortalResult.Ok)
                        this.ActiveEntity = dataPortalContext.ResultValueItem;
                }
                catch (System.Exception ex)
                {
                    exceptions.Add(ex);
                }
                try
                {
                    task = OnSaveItemCompletedAsync(dataPortalContext);
                    if (task != null)
                    {
                        await task;
                    }
                    else
                    {
                        OnSaveItemCompleted(dataPortalContext);
                        await dataPortalContext.DoTaskAsync();
                    }
                }
                catch (System.Exception ex)
                {
                    exceptions.Add(ex);
                }
            }
            await LogAsync(exceptions);
            await Sidvall.UI.SystemContext.Current.DialogManager.ShowDialogErrorMessageAsync(Sidvall.Resources.ResourceKeyManager.Current.SaveErrorTitle, exceptions);
        }

        #endregion
        #region OnBeforeSaveItem

        protected virtual void OnBeforeSaveItem(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
        }

        #endregion
        #region OnBeforeSaveItemAsync

        protected virtual Task OnBeforeSaveItemAsync(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
            return null;
        }

        #endregion
        #region SaveItemAsync

        protected virtual Task<TEntity> SaveItemAsync(TEntity item)
        {
            return null;
        }

        #endregion
        #region OnAfterSaveItem

        protected virtual void OnAfterSaveItem(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
        }

        #endregion
        #region OnAfterSaveItemAsync

        protected virtual Task OnAfterSaveItemAsync(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
            return null;
        }

        #endregion
        #region OnSaveItemCompleted

        protected virtual void OnSaveItemCompleted(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
        }

        #endregion
        #region OnSaveItemCompletedAsync

        protected virtual Task OnSaveItemCompletedAsync(global::Sidvall.Business.DataPortalContext<TEntity, TEntity> dataPortalContext)
        {
            return null;
        }

        #endregion

        // Events
        #region OnActiveEntityChanged

        protected virtual void OnActiveEntityChanged(BusinessObjectChangeMode changeMode, ChildChangedEventArgs childArgs, System.ComponentModel.PropertyChangedEventArgs propertyArgs)
        {
        }

        #endregion
        #region OnIsBusyChanged

        protected override void OnIsBusyChanged()
        {
            RaisePropertyChanged(nameof(IsSavable));
        }

        #endregion
        #region OnIsSavableChanged

        protected virtual void OnIsSavableChanged()
        {
        }

        #endregion
        #region OnViewEventAsync

        protected override async Task OnViewEventAsync(Sidvall.UI.Views.ViewEventArgs args)
        {
            if (args == null)
                throw new System.ArgumentNullException(nameof(args));
            switch (args.EventType)
            {
                case Sidvall.UI.Views.ViewEventType.ApplicationClosing:
                    await SaveOnExitAsync(Sidvall.Resources.ResourceKeyManager.Current.ApplicationClosingTitle);
                    break;
                case Sidvall.UI.Views.ViewEventType.ViewUnloading:
                    // Only save if user is not refreshing
                    if (Sidvall.UI.SystemContext.Current.NavigationManager.CurrentNavigationAction != Navigation.NavigationAction.Refresh)
                    {
                        await SaveOnExitAsync(Sidvall.Resources.ResourceKeyManager.Current.ViewUnloadingTitle);
                    }
                    break;
                case Sidvall.UI.Views.ViewEventType.ViewUnloaded:
                    UnloadViewModel();
                    break;
                default:
                    break;
            }
        }

        #endregion
        #region OnDataRowBusinessRulesPropertyChanged

        protected void OnDataRowBusinessRulesPropertyChanged(object sender, BusinessRulePropertyEventArgs e)
        {
            if (e == null)
                throw new System.ArgumentNullException(nameof(e));
            _ClearErrors = true;
            foreach (var propertyName in e.PropertyNames)
                OnErrorsChanged(propertyName);
            _ClearErrors = false;
            foreach (var propertyName in e.PropertyNames)
                OnErrorsChanged(propertyName);
        }

        #endregion
        #region OnErrorsChanged

        protected void OnErrorsChanged(string propertyName)
        {
            OnErrorsChanged(new DataErrorsChangedEventArgs(propertyName));
        }
        protected virtual void OnErrorsChanged(DataErrorsChangedEventArgs args)
        {
            if (this.ErrorsChanged != null)
                this.ErrorsChanged.Invoke(this, args);
        }

        #endregion

        #region GetDataRowBusinessRulesForProperty

        protected virtual BusinessRuleCollection GetDataRowBusinessRulesForProperty(string propertyName)
        {
            var items = new BusinessRuleCollection();
            if (!string.IsNullOrWhiteSpace(propertyName))
                items.AddRange(true, this.DataRowBusinessRules.FindBusinessRulesForProperties(true, propertyName));
            else
                items.AddRange(true, this.DataRowBusinessRules.FindBusinessRulesForProperties(true));
            return items;
        }

        #endregion
        #region GetIsSavableResolved

        protected virtual bool GetIsSavableResolved(TEntity item)
        {
            return item.DataRowStateIsDirty;
        }

        #endregion

        #region INotifyDataErrorInfo Members

        System.Collections.IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName)
        {
            if (_ClearErrors)
                return null;
            var items = GetDataRowBusinessRulesForProperty(propertyName);
            if (items.Count == 0)
                return null;
            var value = new System.Collections.Generic.List<BusinessRuleCollection>();
            value.Add(items);
            return value;
        }
        bool INotifyDataErrorInfo.HasErrors
        {
            get
            {
                return this.DataRowBusinessRules.Count > 0;
            }
        }

        #endregion

        #endregion
        #region Private Members

        private readonly object _Mutex = new object();
        private bool _ClearErrors = false;

        #region LogManager

        private Sidvall.Logging.LogManager LogManager
        {
            get
            {
                return Sidvall.SystemContext.Current.LogManager;
            }
        }

        #endregion

        #region OnActiveEntityChanging

        private void OnActiveEntityChanging(object oldValue, object newValue)
        {
            RemoveEventHooks(oldValue);
            AddEventHooks(newValue);
        }

        #endregion
        #region OnActiveEntityChildChanged

        private void OnActiveEntityChildChanged(object sender, ChildChangedEventArgs e)
        {
            if (this.ActiveEntity != null)
            {
                this.IsSavable = GetIsSavableResolved(this.ActiveEntity);
                if (e.CollectionChangedArgs != null)
                {
                    OnActiveEntityChanged(BusinessObjectChangeMode.ChildCollectionChanged, e, null);
                }
                else
                {
                    switch (e.PropertyChangedArgs.PropertyName)
                    {
                        case nameof(BusinessBase.DataRowBusinessRules):
                        case nameof(BusinessBase.DataRowStateIsDeleted):
                        case nameof(BusinessBase.DataRowStateIsDirty):
                        case nameof(BusinessBase.DataRowStateIsSelfDirty):
                        case nameof(BusinessBase.DataRowStateIsNew):
                        case nameof(BusinessBaseCollection<TEntity>.TotalRowCount):
                            OnActiveEntityChanged(BusinessObjectChangeMode.ChildMetaPropertyChanged, e, null);
                            break;
                        default:
                            OnActiveEntityChanged(BusinessObjectChangeMode.ChildPropertyChanged, e, null);
                            break;
                    }
                }
            }
        }

        #endregion
        #region OnActiveEntityPropertyChanged

        private void OnActiveEntityPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (this.ActiveEntity != null)
            {
                this.IsSavable = GetIsSavableResolved(this.ActiveEntity);
                switch (e.PropertyName)
                {
                    case nameof(BusinessBase.DataRowBusinessRules):
                    case nameof(BusinessBase.DataRowStateIsDeleted):
                    case nameof(BusinessBase.DataRowStateIsDirty):
                    case nameof(BusinessBase.DataRowStateIsSelfDirty):
                    case nameof(BusinessBase.DataRowStateIsNew):
                        OnActiveEntityChanged(BusinessObjectChangeMode.MetaPropertyChanged, null, e);
                        break;
                    default:
                        OnActiveEntityChanged(BusinessObjectChangeMode.PropertyChanged, null, e);
                        break;
                }
            }
        }

        #endregion

        #region AddEventHooks

        private void AddEventHooks(object item)
        {
            if (item == null)
                return;
            var pc = item as INotifyPropertyChanged;
            if (pc != null)
                pc.PropertyChanged += OnActiveEntityPropertyChanged;
            var cc = item as INotifyChildChanged;
            if (cc != null)
                cc.ChildChanged += OnActiveEntityChildChanged;
        }

        #endregion
        #region RemoveEventHooks

        private void RemoveEventHooks(object item)
        {
            if (item == null)
                return;
            var pc = item as INotifyPropertyChanged;
            if (pc != null)
                pc.PropertyChanged -= OnActiveEntityPropertyChanged;
            var cc = item as INotifyChildChanged;
            if (cc != null)
                cc.ChildChanged -= OnActiveEntityChildChanged;
        }

        #endregion
        #region SaveOnExitAsync

        private async Task<bool> SaveOnExitAsync(string title)
        {
            if (!this.ViewModelSettings.SaveOnExit)
                return false;
            if (this.IsSavable)
            {
                var message = await Sidvall.UI.SystemContext.Current.DialogManager.ShowYesNoMessageAsync(title,
                    Sidvall.Resources.ResourceKeyManager.Current.SaveChangesMessage, Sidvall.UI.Dialogs.MessageType.Question, true);
                if (message.AnswerIsYes)
                {
                    await DoSaveAsync();
                }
                this.IsSavable = false;
                return true;
            }
            return false;
        }

        #endregion
        #region LogAsync

        private async Task LogAsync(System.Exception exception)
        {
            if (this.LogManager != null)
                await this.LogManager.WriteAsync(exception, Sidvall.Logging.LogCategories.Application);
        }
        private async Task LogAsync(System.Collections.Generic.IEnumerable<System.Exception> exceptions)
        {
            if (this.LogManager != null)
                await this.LogManager.WriteAsync(exceptions, Sidvall.Logging.LogCategories.Application);
        }

        #endregion

        #endregion
    }
}
